Skip to content

release: v0.3.0 - #22

Merged
sturdy-robot merged 13 commits into
mainfrom
egui
Mar 3, 2026
Merged

release: v0.3.0#22
sturdy-robot merged 13 commits into
mainfrom
egui

Conversation

@sturdy-robot

@sturdy-robot sturdy-robot commented Mar 3, 2026

Copy link
Copy Markdown
Owner

Add egui to replace notan.

Add a bunch of new improvements to the emulator:

  • New frontend
  • Support for ZIP files
  • Load directories and load ROMs directly from the emulator window
  • List ROMs, sort and search them
  • Different color palettes: Greyscale, Greenscale, Pocket
  • Scaling and resizing emulator window
  • Fix: HALT bug behavior
  • Customizable Joypad keys
  • WASM support
  • macOS support
  • Windows on ARM64 support
  • Control on emulation: Pause, resume, stop, restart
  • Fullscreen support

Summary by Sourcery

Release version 0.3.0 with a new egui-based frontend, expanded platform targets, and core emulation improvements.

New Features:

  • Introduce an egui/eframe-based frontend with a native window, menus, scaling modes, palettes, and configurable keybindings.
  • Add support for loading ROMs from ZIP archives and from in-memory bytes, enabling directory-based game browsing and a WASM frontend.
  • Expose a wasm32-compatible frontend library entry point and Web runner for running the emulator in the browser.
  • Include cartridge metadata parsing for company names and improved ROM title handling.
  • Add macOS universal app bundle packaging, Linux AppImage packaging, and broadened release targets including Apple silicon and additional Linux/Windows architectures.

Bug Fixes:

  • Correct the HALT bug and interrupt handling behavior to match Game Boy hardware semantics.
  • Fix cartridge RAM size for the 0x04 RAM size code to use the correct 0x20000 value.
  • Ensure serial output retrieval clears the underlying buffer to avoid duplicated messages.

Enhancements:

  • Refactor the frontend into a dedicated app module with audio handling, game library UI, and persistent configuration.
  • Export core cartridge and joypad types and add new helpers to construct a Game Boy instance from ROM bytes.
  • Enable serial message printing during emulation for easier debugging and test output.
  • Improve README with updated screenshots, feature list, build/run instructions, and documented WASM and platform support.

Build:

  • Bump crate versions for core and frontend to 0.3.0 and adjust frontend crate layout to support both bin and cdylib builds.
  • Add wasm-specific dependencies and configuration to support WebAssembly builds across core and frontend crates.
  • Introduce a Windows build script to embed an application icon in native binaries.

CI:

  • Update cargo-dist configuration and GitHub release workflow targets to include macOS (Intel/Apple silicon) and additional Linux/Windows platforms.
  • Add a new GitHub Actions workflow to produce macOS .app bundles and Linux AppImage artifacts and attach them to releases.

Documentation:

  • Refresh README screenshots and simplify positioning statements while expanding documentation on core features, frontend, keybindings, platform support, and detailed build instructions.

- Add game library browser showing ROM title, company, and filename
- Implement asynchronous directory loading with progress indication
- Add configurable keybinds with persistence via serde
- Add display scaling modes (integer scaling and stretch)
- Add color palette options (greyscale, classic green, pocket)
- Expose cartridge module and add company name lookup from licensee codes
- Add FPS counter and improve title extraction
- Add APP_NAME constant combining name and version from Cargo.toml
- Replace hardcoded "SturdyGB" strings with APP_NAME constant
- Simplify title formatting by reusing constant
- Remove unused SCALE constant
- Add detailed build instructions for desktop and WebAssembly targets
- Add search and sort functionality to game library browser
- Update dependencies (rfd 0.17.2, cpal 0.17)
- Replace std::time::Instant with instant crate for WASM compatibility
- Add web-specific UI for ROM loading on WASM target
- Fix copyright year in release workflow (2024)
- Enable serial message printing for debugging
- Reorganize README sections and improve
- Add halt_bug flag to CPU state to track HALT bug condition
- Implement HALT bug when HALT executed with IME=0 and pending interrupts
- Fix PC increment behavior during HALT bug (decrement by 1)
- Simplify interrupt handling by checking pending interrupts consistently
- Remove redundant is_halted check when pushing PC to stack
- Mask interrupt flags with 0x1F to check only valid interrupt bits
- Add optional save_path parameter to load_cartridge_from_bytes and build_from_bytes
- Generate .sav file path when loading ROMs from filesystem
- Pass None for save_path when loading ROMs asynchronously (web/memory)
- Extract save path generation to avoid duplication in load_rom_file
- Fix RAM size 0x04 from 0x200000 to 0x20000 (128KB)
- Clear serial data buffer after reading to prevent duplicates
- Optimize game library loading by reading only header bytes instead of full ROM
- Add separate handling for ZIP files when reading ROM headers
- Cap leftover audio buffer at 8192 samples to prevent unbounded growth
- Configure WGPU to prefer low power and use GL/Metal backends
- Remove unnecessary to_owned() call in serial
- This avoid inconsistencies when SturdyGB hits other versions
- Replace GitHub-hosted screenshots with local images in ./images directory
- Increase screenshot display size from 300px to 400px
- Fix table layout by adding missing closing td tag
- Add winresource build dependency for Windows executable icon
- Update SVG export settings to 512x512 resolution
- Add pause/resume and reset options to Emulation menu
- Store ROM bytes and save path in State for reset functionality
- Skip Windows resource compilation when targeting wasm32
- Fix paused state management when stopping or loading ROMs
- Only process input and run emulation loop when not paused
- Improve WASM welcome screen with app name and instructions
- Clone ROM bytes and save path when loading to enable reset
- Add fullscreen toggle via F11 key and View menu
- Store fullscreen state in SturdyConfig with persistence
- Add emoji icons to menu items for better visual clarity
- Support ZIP files in ROM file picker dialogs
- Move Stop button from File menu to Emulation menu
- Improve game library table column sizing with auto-sizing
- Update file filter to include .zip extension for both native and WASM
- Add cfg(not(target_arch = "wasm32")) guards to fullscreen field and logic
- Fullscreen API not supported in WASM, causing compilation issues
- Add folder emoji icons to ROM picker buttons on WASM welcome screen
- Fix WASM file filter to include .zip instead of .gbc extension
@sourcery-ai

sourcery-ai Bot commented Mar 3, 2026

Copy link
Copy Markdown

Reviewer's Guide

Replaces the Notan-based frontend with a new egui/eframe UI (including ROM browser, scaling, palettes, configurable keybindings, and WASM support), adds cartridge/ROM loading from bytes and ZIP archives, fixes HALT/interrupt behavior and other core emulation details, and introduces multi-platform packaging for desktop (macOS, Linux, Windows) while updating documentation and versions for the v0.3.0 release.

Sequence diagram for ROM loading and initialization (file/ZIP to running GB)

sequenceDiagram
    actor User
    participant EmuApp
    participant FileDialog
    participant Fs as Filesystem
    participant GbInstance
    participant CartridgeModule
    participant CartridgeHeader
    participant Audio as AudioGlobals

    User->>EmuApp: Click Open_ROM
    EmuApp->>FileDialog: show_open_dialog(extensions: [gb,zip])
    FileDialog-->>User: Choose_file_path
    User-->>FileDialog: Confirm
    FileDialog-->>EmuApp: Path
    EmuApp->>Fs: read(path)
    Fs-->>EmuApp: Vec_u8_bytes

    alt Bytes_is_ZIP
        EmuApp->>EmuApp: extract_rom_from_bytes(bytes)
        EmuApp-->>EmuApp: rom_bytes
    else Raw_ROM
        EmuApp->>EmuApp: bytes_used_as_rom
    end

    EmuApp->>GbInstance: build_from_bytes(rom_bytes, Some(save_path))
    GbInstance->>CartridgeModule: load_cartridge_from_bytes(rom_bytes, Some(save_path))
    CartridgeModule->>CartridgeHeader: new(rom_bytes)
    CartridgeHeader-->>CartridgeModule: CartridgeHeader
    CartridgeModule-->>GbInstance: (Box_Mbc, GbMode)
    GbInstance->>GbInstance: Determine_GbTypes_from_GbMode
    GbInstance-->>EmuApp: Gb

    EmuApp->>Audio: setup_audio(&mut Gb)
    Audio->>Audio: create_cpal_stream_and_channel()
    Audio-->>EmuApp: AUDIO_PRODUCER_initialized

    EmuApp->>EmuApp: Create_State(gb, rgba_buffer, leftover_audio)
    EmuApp-->>User: ROM_running_with_video_audio
Loading

Sequence diagram for HALT and interrupt handling bug fix

sequenceDiagram
    participant Gb
    participant Cpu

    Note over Gb,Cpu: Each CPU tick
    Gb->>Gb: handle_interrupt()
    Gb->>Cpu: check interrupt_master
    alt Interrupts_disabled
        Gb-->>Cpu: Return_no_interrupt
    else Interrupts_enabled
        Gb->>Gb: pending = ie_flag & if_flag & 0x1F
        alt pending == 0
            Gb-->>Cpu: Return_no_interrupt
        else pending != 0
            alt Cpu.is_halted
                Gb->>Cpu: is_halted = false
            end
            Gb->>Cpu: interrupt_master = false
            Gb->>Cpu: sp = sp - 2
            Gb->>Gb: write_word(sp, pc)
            Gb->>Gb: interrupt_source = get_interrupt_source(pending)
            Gb->>Gb: pc = go_interrupt(interrupt_source)
            Gb->>Gb: if_flag &= !interrupt_source
            Cpu->>Cpu: pending_cycles += 5
        end
    end

    Note over Cpu,Gb: Executing HALT instruction
    Gb->>Gb: halt()
    Gb->>Cpu: advance_pc()
    Gb->>Gb: pending = ie_flag & if_flag & 0x1F
    alt !interrupt_master && pending != 0
        Gb->>Cpu: halt_bug = true
    else
        Gb->>Cpu: is_halted = true
    end

    Note over Cpu: advance_pc with halt_bug
    Cpu->>Cpu: advance_pc()
    Cpu->>Cpu: adv = OPCODES_SIZE[current_instruction]
    alt halt_bug == true
        Cpu->>Cpu: halt_bug = false
        Cpu->>Cpu: adv = adv - 1 (saturating)
    end
    Cpu->>Cpu: pc = pc + adv
Loading

ER diagram for updated cartridge metadata (title and company)

erDiagram
    CARTRIDGE_HEADER {
        string title
        string company
        integer rom_size
        integer ram_size
        string mbc_type
        boolean sgb_flag
        integer cgb_flag
    }

    CARTRIDGE_FILE {
        string file_path
        string filename
        string extension
    }

    COMPANY_CODE {
        string old_code
        string new_code
        string company_name
    }

    CARTRIDGE_FILE ||--|| CARTRIDGE_HEADER : "contains_header"
    COMPANY_CODE ||--o{ CARTRIDGE_HEADER : "decoded_to_company"

    %% Mapping functions (conceptual relationships)
    FUNCTION_load_cartridge_from_bytes {
        string rom_bytes
        string save_path
    }

    FUNCTION_get_company_name {
        string old_code
        string new_code
    }

    FUNCTION_load_cartridge_from_bytes ||--|| CARTRIDGE_HEADER : "parses_header"
    FUNCTION_get_company_name ||--|| COMPANY_CODE : "returns_name"
Loading

Class diagram for updated core emulator types (v0.3.0)

classDiagram
    class Gb {
        +u8 ie_flag
        +u8 if_flag
        +Cpu cpu
        +void run_one_frame()
        +void handle_interrupt()
        +void cpu_tick()
        +void components_tick()
        +void print_serial_message()
        +void set_sample_rate(u32 sample_rate)
        +[[f32;2]] get_audio_buffer()
        +[[u8;160];144] get_screen_data()
        +void write_word(u16 addr, u16 value)
    }

    class Cpu {
        +u16 pc
        +u16 sp
        +bool interrupt_master
        +bool is_halted
        +bool halt_bug
        +u8 current_instruction
        +i32 pending_cycles
        +void advance_pc()
    }

    class CartridgeHeader {
        +[u8;4] entry
        +[u8;48] logo
        +String title
        +u8 cgb_flag
        +bool sgb_flag
        +MBCTypes mbc_type
        +u32 rom_size
        +u32 ram_size
        +String company
        +new(rom_data: [u8]) Result~CartridgeHeader,&'static str~
    }

    class GbInstance {
        +static build(path: &str) Result~Gb,String~
        +static build_from_bytes(rom_data: Vec~u8~, save_path: Option~PathBuf~) Result~Gb,String~
    }

    class Serial {
        +Vec~u8~ serial_data
        +Option~String~ get_serial_message()
    }

    class JoypadButton {
        <<enum>>
        +A
        +B
        +Start
        +Select
        +Up
        +Down
        +Left
        +Right
    }

    class Mbc {
        <<trait>>
        +u8 read_rom(u16 address)
        +void write_rom(u16 address, u8 value)
        +u8 read_ram(u16 address)
        +void write_ram(u16 address, u8 value)
        +void save_ram()
    }

    class GbMode {
        <<enum>>
        +DmgMode
        +CgbMode
    }

    class GbTypes {
        <<enum>>
        +Dmg
        +Cgb
    }

    class CartridgeModule {
        +load_cartridge(filename: &str) Result~Box~dyn Mbc~,GbMode,String~
        +load_cartridge_from_bytes(rom_data: Vec~u8~, save_path: Option~PathBuf~) Result~Box~dyn Mbc~,GbMode,String~
        +get_company_name(old_code: u8, new_code: &[u8]) String
    }

    Gb o-- Cpu
    GbInstance ..> Gb
    GbInstance ..> CartridgeModule
    CartridgeModule ..> CartridgeHeader
    CartridgeModule ..> Mbc
    CartridgeHeader --> MBCTypes
    Gb --> GbMode
    Gb --> GbTypes
    Serial --> Gb
    JoypadButton <.. serde_Serialize
    JoypadButton <.. serde_Deserialize

    class serde_Serialize {
        <<trait>>
    }

    class serde_Deserialize {
        <<trait>>
    }
Loading

Class diagram for new egui/eframe frontend (EmuApp)

classDiagram
    class EmuApp {
        +Option~State~ state
        +Option~TextureHandle~ texture
        +Option~String~ error_msg
        +(Sender~Result~Vec~u8~,String~~, Receiver~Result~Vec~u8~,String~~) rom_load_channel
        +bool paused
        +SturdyConfig config
        +bool show_options
        +instant::Instant start_time
        +usize frames_rendered
        +instant::Instant last_fps_update
        +usize current_fps
        +new(cc: &CreationContext, initial_rom: Option~String~) EmuApp
        +void load_rom_file(path: &str)
        +void load_rom_bytes(bytes: Vec~u8~, save_path: Option~PathBuf~)
        +void update(ctx: &egui::Context, frame: &mut eframe::Frame)
        +void save(storage: &mut dyn eframe::Storage)
    }

    class State {
        +Gb gb
        +Vec~u8~ rgba
        +Vec~[f32;2]~ leftover_audio
        +String title
        +Vec~u8~ rom_bytes
        +Option~PathBuf~ save_path
    }

    class SturdyConfig {
        +ScaleMode scale
        +Palette palette
        +Vec~PathBuf~ rom_directories
        +HashMap~JoypadButton,egui::Key~ keybinds
        +bool fullscreen
    }

    class ScaleMode {
        <<enum>>
        +Integer(f32)
        +Stretch
    }

    class Palette {
        <<enum>>
        +Greyscale
        +ClassicGreen
        +Pocket
    }

    class GameEntry {
        +PathBuf path
        +String filename
        +String title
        +String company
    }

    class SortMethod {
        <<enum>>
        +Filename
        +Title
        +Company
    }

    class AudioGlobals {
        +static Option~SyncSender~[f32;2]~~ AUDIO_PRODUCER
        +static Option~cpal::Stream~ AUDIO_STREAM
        +setup_audio(gb: &mut Gb)
    }

    class FrontendLib {
        +APP_NAME: &str
        +extract_rom_from_bytes(bytes: &[u8]) Option~Vec~u8~~
        +set_btn(ctx: &egui::Context, state: &mut State, key: egui::Key, btn: JoypadButton)
    }

    EmuApp o-- State
    EmuApp --> SturdyConfig
    EmuApp --> GameEntry
    EmuApp --> SortMethod
    EmuApp --> AudioGlobals
    EmuApp --> FrontendLib

    State --> Gb
    State --> JoypadButton

    SturdyConfig --> ScaleMode
    SturdyConfig --> Palette
    SturdyConfig --> JoypadButton

    GameEntry --> CartridgeHeader

    AudioGlobals ..> cpal_Host
    AudioGlobals ..> cpal_Device
    AudioGlobals ..> cpal_Stream

    class cpal_Host {
        <<external>>
    }

    class cpal_Device {
        <<external>>
    }

    class cpal_Stream {
        <<external>>
    }

    class TextureHandle {
        <<external>>
    }

    class Gb {
    }

    class JoypadButton {
    }

    class CartridgeHeader {
    }
Loading

File-Level Changes

Change Details Files
Replace Notan frontend with egui/eframe-based application supporting desktop and WASM, with configurable UI and emulation controls.
  • Remove Notan-based main loop, rendering, and input handling and replace with eframe::run_native main entry on native platforms and a WASM-specific main on wasm32
  • Introduce EmuApp egui application managing Gb instance, frame rendering, audio pipeline, errors, and FPS tracking
  • Implement configurable scaling modes (integer and stretch-to-window), color palettes, and an options window persisted via egui storage
  • Integrate CPAL-based audio output into the egui app with buffered channel and leftover audio handling for smooth playback
  • Add WASM startup helpers and crate lib entry to expose start() for web builds
crates/frontend/src/main.rs
crates/frontend/src/app.rs
crates/frontend/src/lib.rs
Add ROM loading from bytes (including ZIP archives) and enhance cartridge metadata (company name, header parsing).
  • Expose cartridge module publicly and add load_cartridge_from_bytes that builds an MBC from in-memory ROM data plus optional save path
  • Add CartridgeHeader.company field and compute it with a licensee code lookup, while tightening RAM size calculation and title parsing to drop NULs and trim
  • Support extracting .gb/.gbc from ZIP files both for ad-hoc ROM loading and directory scanning, building appropriate save paths
  • Expose GbInstance::build_from_bytes to construct a Game Boy instance directly from ROM bytes and optional save path
crates/core/src/cartridge.rs
crates/core/src/lib.rs
crates/core/src/prelude.rs
crates/frontend/src/app.rs
Implement ROM browser, directory scanning, search, sort, and metadata listing in the desktop frontend.
  • Add GameEntry and SortMethod types and maintain a game_list in EmuApp on non-wasm targets
  • Scan user-selected directories (optionally recursively) with walkdir, asynchronously collecting .gb/.gbc/.zip entries and extracting header-based title and company
  • Render a searchable, sortable table of games using egui_extras::TableBuilder, with double-click to load the selected ROM and a chips UI for managing directories
crates/frontend/src/app.rs
Make joypad bindings configurable and suitable for serialization, and wire them into egui input handling.
  • Extend JoypadButton with serde Serialize/Deserialize and common trait derives so it can be used as a key in HashMap<JoypadButton, egui::Key>
  • Add SturdyConfig with keybinds map, default bindings, fullscreen and palette/scale settings, and persist it via eframe storage
  • Implement keybinding editing UI that captures the next key press per button and updates config, and use the resulting mapping in per-frame input handling
crates/core/src/joypad.rs
crates/frontend/src/app.rs
Fix HALT bug and interrupt handling semantics to better match hardware behavior.
  • Change Gb::halt to set a CPU halt_bug flag (instead of halting) when IME is disabled but an interrupt is pending, and only enter halted state otherwise
  • Update Cpu::advance_pc to honor the halt_bug flag by advancing the PC by one less byte for the next instruction and then clearing the flag
  • Rework Gb::handle_interrupt to compute pending interrupts once, clear is_halted when any are pending, and always push the current PC (no +1) before jumping
  • Limit get_interrupt_source to enabled-and-requested bits (0x1F mask) for consistent priority resolution
crates/core/src/instructions.rs
crates/core/src/cpu.rs
crates/core/src/interrupts.rs
Minor core behavior and API adjustments to improve debugging and correctness.
  • Change Serial::get_serial_message to clear the underlying buffer after returning a string so repeated calls don’t re-emit the same data
  • Enable serial prints each CPU step by calling print_serial_message in Gb::step
  • Adjust cartridge RAM size decoding for code 0x04 to 0x20000 bytes instead of 0x200000
crates/core/src/serial.rs
crates/core/src/gb.rs
crates/core/src/cartridge.rs
Update crate manifests, dependencies, and platform-specific configuration for v0.3.0 with egui, WASM, and platform packaging support.
  • Bump sturdygb and sturdygb_core crate versions to 0.3.0 and refactor frontend crate to have a cdylib/rlib lib plus sturdygb_bin binary
  • Replace Notan dependency with eframe/egui/egui_extras and add zip, image, serde, instant, walkdir, wasm-bindgen(-futures), web-sys, getrandom variants, and CPAL wasm-bindgen features
  • Add Windows build script to embed application icon via winresource and configure WGPU backend preferences for GL/Metal and LowPower
  • Add getrandom dependency for wasm32 in core for RNG support
crates/frontend/Cargo.toml
crates/core/Cargo.toml
crates/frontend/build.rs
dist-workspace.toml
Improve documentation, screenshots, and build/run instructions reflecting the new frontend and platforms.
  • Swap README screenshots to local images and simplify project positioning (less emphasis on incompleteness)
  • Update feature lists to mention egui frontend, remove Notan and SDL references, adjust roadmap and warnings, and describe new keybindings behavior
  • Rewrite build/run section to cover desktop release builds, wasm32 target setup, wasm-bindgen usage, and running a local web server
README.md
Add CI packaging workflows for macOS .app bundles and Linux AppImage, and update existing release config.
  • Introduce .github/workflows/package.yml that builds universal macOS app bundle with icon/Info.plist and x86_64 Linux AppImage using linuxdeploy, uploading artifacts and release assets
  • Adjust dist-workspace targets to include macOS/aarch64 and x86_64 variants and correct copyright year in release workflow
.github/workflows/package.yml
.github/workflows/release.yml
dist-workspace.toml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • In EmuApp::update and related input handling you unconditionally unwrap() keybind lookups from config.keybinds, which can panic if the stored config is missing entries (e.g., after format changes); consider providing defaults or handling missing keys more defensively.
  • The async directory loader in update uses rx.try_recv() both in the while let Ok(entry) loop and again afterwards to detect Disconnected, which can consume and drop an extra entry or mis-detect completion; instead track completion via the loop result or use recv()/recv_timeout() once to determine when the sender is closed.
  • The global AUDIO_PRODUCER/AUDIO_STREAM statics use static mut without any synchronization besides the implicit main-thread usage assumption; if you expect multi-threaded use or hot-reload scenarios, consider replacing them with OnceLock<Mutex<...>> or a similar safe abstraction to avoid potential data races.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `EmuApp::update` and related input handling you unconditionally `unwrap()` keybind lookups from `config.keybinds`, which can panic if the stored config is missing entries (e.g., after format changes); consider providing defaults or handling missing keys more defensively.
- The async directory loader in `update` uses `rx.try_recv()` both in the `while let Ok(entry)` loop and again afterwards to detect `Disconnected`, which can consume and drop an extra entry or mis-detect completion; instead track completion via the loop result or use `recv()`/`recv_timeout()` once to determine when the sender is closed.
- The global `AUDIO_PRODUCER`/`AUDIO_STREAM` statics use `static mut` without any synchronization besides the implicit main-thread usage assumption; if you expect multi-threaded use or hot-reload scenarios, consider replacing them with `OnceLock<Mutex<...>>` or a similar safe abstraction to avoid potential data races.

## Individual Comments

### Comment 1
<location path="crates/frontend/src/app.rs" line_range="704-713" />
<code_context>
+                    let k = &self.config.keybinds;
</code_context>
<issue_to_address>
**issue (bug_risk):** Unwrapping keybind lookups can panic when loading configs that don’t define all bindings.

`SturdyConfig` keybinds are accessed with `k.get(&JoypadButton::X).unwrap()`, so any missing mapping (e.g., from older, edited, or corrupted configs) will panic at load time. Instead of unwrapping, fall back to a default mapping for missing entries so that legacy or malformed configs don’t crash the app.
</issue_to_address>

### Comment 2
<location path="crates/frontend/src/app.rs" line_range="411-412" />
<code_context>
+                    if ui.button("📁 Open ROM...").clicked() {
+                        #[cfg(not(target_arch = "wasm32"))]
+                        {
+                            if let Some(path) = FileDialog::new()
+                                .add_filter("GameBoy ROMs", &["gb", "zip"])
+                                .pick_file()
+                            {
</code_context>
<issue_to_address>
**suggestion (bug_risk):** The desktop file picker omits `.gbc` from the ROM filter, unlike other code paths.

Here you only allow `"gb"` and `"zip"`, but elsewhere you treat both `.gb` and `.gbc` as valid ROMs. To keep behavior consistent and let users open `.gbc` files directly, please add `"gbc"` to this filter (and any related `FileDialog` / `AsyncFileDialog` filters you want aligned).

Suggested implementation:

```rust
                            if let Some(path) = FileDialog::new()
                                .add_filter("GameBoy ROMs", &["gb", "gbc", "zip"])
                                .pick_file()
                            {

```

There may be other `FileDialog` or `AsyncFileDialog` usages in this file or elsewhere that also define ROM filters. For consistency, update those filters in the same way (add `"gbc"` alongside `"gb"` and `"zip"`) so that all file-picking code paths accept `.gbc` ROMs directly.
</issue_to_address>

### Comment 3
<location path="README.md" line_range="177" />
<code_context>
+5. **Serve the application:**
+   You will need a local web server to serve the files in the `crates/frontend/public` directory. For example, using Python:
+   ```bash
+   cd public
+   python -m http.server 8080
+   ```
</code_context>
<issue_to_address>
**issue:** Clarify the path here to match `crates/frontend/public` mentioned above.

Since earlier steps already run `cd crates/frontend`, it would be clearer and less error‑prone to either use `cd crates/frontend/public` here or explicitly state what the current working directory should be, so readers don’t accidentally serve the wrong path.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread crates/frontend/src/app.rs Outdated
Comment thread crates/frontend/src/app.rs Outdated
Comment thread README.md Outdated
- Replace unsafe static mut with Mutex-wrapped statics for audio components
- Add SturdyConfig::default_key() and keybind() helper methods
- Simplify input handling loop using button array iteration
- Fix directory loading channel disconnection detection
- Add .gbc extension to all file picker filters
- Fix README web server path instructions to be relative to crates/frontend
- Remove unsafe blocks and static_mut_refs lint allowances
@sturdy-robot
sturdy-robot merged commit 26fba9f into main Mar 3, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant